using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;

namespace AssetImporter
{
    public class Shader
    {
        internal String programName;
        private int handle;
        private String vertexShaderSource;
        private String fragmentShaderSource;
        private CachedTransform<String, int> uniformHandleCache;

        internal Shader(in String program_name)
        {
            programName = program_name;
            handle = GL.CreateProgram();
            uniformHandleCache = new CachedTransform<string, int>((String name) => GL.GetUniformLocation(handle, name));
            vertexShaderSource = "";
            fragmentShaderSource = "";
        }

        ~Shader()
        {
            GL.DeleteProgram(handle);
        }

        internal void LoadVertexShaderSource(in String source)
        {
            vertexShaderSource = source;
        }

        internal void LoadFragmentShaderSource(in String source)
        {
            fragmentShaderSource = source;
        }

        internal void CompileAndLink()
        {
            var vShader = GL.CreateShader(ShaderType.VertexShader);
            var fShader = GL.CreateShader(ShaderType.FragmentShader);
            
            if(vertexShaderSource.Any())
            {
                GL.ShaderSource(vShader, vertexShaderSource);
                GL.CompileShader(vShader);
                GL.GetShader(vShader, ShaderParameter.CompileStatus, out int success);
                if (success == 0)
                {
                    var info = GL.GetShaderInfoLog(vShader);
                    throw new Exception($"GL.CompileShader for shader '{programName}' [Vertex Shader] had info log:\n{info}");
                }
                GL.AttachShader(handle, vShader);
            }

            if(fragmentShaderSource.Any())
            {
                GL.ShaderSource(fShader, fragmentShaderSource);
                GL.CompileShader(fShader);
                GL.GetShader(fShader, ShaderParameter.CompileStatus, out int success);
                if (success == 0)
                {
                    var info = GL.GetShaderInfoLog(fShader);
                    throw new Exception($"GL.CompileShader for shader '{programName}' [Fragment Shader] had info log:\n{info}");
                }
                GL.AttachShader(handle, fShader);
            }

            GL.LinkProgram(handle);
            {
                GL.GetProgram(handle, GetProgramParameterName.LinkStatus, out var success);
                if(success == 0)
                {
                    var info = GL.GetProgramInfoLog(handle);
                    throw new Exception($"GL.LinkProgram had info log [{programName}]:\n{info}");
                }
            }

            GL.DetachShader(handle, vShader);
            GL.DetachShader(handle, fShader);

            GL.DeleteShader(vShader);
            GL.DeleteShader(fShader);
        }

        public void UniformBlockBinding(in String name, int value)
        {
            GL.UniformBlockBinding(handle, GL.GetUniformBlockIndex(handle, name), value);
            if(GL.GetError() != ErrorCode.NoError)
                throw new Exception($"Uniform block binding error: {name} {value}");
        }

        public void UseProgram()
        {
            GL.UseProgram(handle);
        }

        public void SetInt(in String name, int value)
        {
            GL.ProgramUniform1(handle, uniformHandleCache.Get(name), value);
        }

        public void SetFloat(in String name, float value)
        {
            GL.ProgramUniform1(handle, uniformHandleCache.Get(name), value);
        }

        public void SetDouble(in String name, double value)
        {
            GL.ProgramUniform1(handle, uniformHandleCache.Get(name), value);
        }

        public void SetVector2(in String name, Vector2 vector)
        {
            GL.ProgramUniform2(handle, uniformHandleCache.Get(name), vector);
        }

        public void SetVector2(in String name, float x, float y)
        {
            GL.ProgramUniform2(handle, uniformHandleCache.Get(name), new Vector2(x, y));
        }

        public void SetVector3(in String name, Vector3 vector)
        {
            GL.ProgramUniform3(handle, uniformHandleCache.Get(name), vector);
        }

        public void SetVector3(in String name, float x, float y, float z)
        {
            GL.ProgramUniform3(handle, uniformHandleCache.Get(name), new Vector3(x, y, z));
        }

        public void SetMatrix4(in String name, Matrix4 mat)
        {
            GL.ProgramUniformMatrix4(handle, uniformHandleCache.Get(name), false, ref mat);
        }
    }

    public class ShaderFromFile : Shader
    {
        public ShaderFromFile(in String program_name, in String path) : base(program_name)
        {
            var vShaderFilePath = path + ".vert";
            var fShaderFilePath = path + ".frag";
            if(!File.Exists(vShaderFilePath))
                throw new FileNotFoundException($"{vShaderFilePath} not found.");
            if(!File.Exists(fShaderFilePath))
                throw new FileNotFoundException($"{fShaderFilePath} not found.");
            LoadVertexShaderSource(File.ReadAllText(vShaderFilePath));
            LoadFragmentShaderSource(File.ReadAllText(fShaderFilePath));
            CompileAndLink();
        }
    }
}
